/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {
  DYNAMIC_TYPE,
  TmplAstComponent,
  TmplAstDirective,
  TmplAstElement,
  TmplAstHostElement,
  TmplAstLetDeclaration,
  TmplAstReference,
  TmplAstTemplate,
  TmplAstVariable,
} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import type {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {addParseSpanInfo} from '../diagnostics';
import {tsCreateVariable} from '../ts_util';
import {getAnyExpression} from '../expression';

/** Types that can referenced locally in a template. */
export type LocalSymbol =
  | TmplAstElement
  | TmplAstTemplate
  | TmplAstVariable
  | TmplAstLetDeclaration
  | TmplAstReference
  | TmplAstHostElement
  | TmplAstComponent
  | TmplAstDirective;

/**
 * A `TcbOp` which creates a variable for a local ref in a template.
 * The initializer for the variable is the variable expression for the directive, template, or
 * element the ref refers to. When the reference is used in the template, those TCB statements will
 * access this variable as well. For example:
 * ```ts
 * var _t1 = document.createElement('div');
 * var _t2 = _t1;
 * _t2.value
 * ```
 * This operation supports more fluent lookups for the `TemplateTypeChecker` when getting a symbol
 * for a reference. In most cases, this isn't essential; that is, the information for the symbol
 * could be gathered without this operation using the `BoundTarget`. However, for the case of
 * ng-template references, we will need this reference variable to not only provide a location in
 * the shim file, but also to narrow the variable to the correct `TemplateRef<T>` type rather than
 * `TemplateRef<any>` (this work is still TODO).
 *
 * Executing this operation returns a reference to the directive instance variable with its inferred
 * type.
 */
export class TcbReferenceOp extends TcbOp {
  constructor(
    private readonly tcb: Context,
    private readonly scope: Scope,
    private readonly node: TmplAstReference,
    private readonly host: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
    private readonly target: TypeCheckableDirectiveMeta | TmplAstTemplate | TmplAstElement,
  ) {
    super();
  }

  // The statement generated by this operation is only used to for the Type Checker
  // so it can map a reference variable in the template directly to a node in the TCB.
  override readonly optional = true;

  override execute(): ts.Identifier {
    const id = this.tcb.allocateId();
    let initializer: ts.Expression =
      this.target instanceof TmplAstTemplate || this.target instanceof TmplAstElement
        ? this.scope.resolve(this.target)
        : this.scope.resolve(this.host, this.target);

    // The reference is either to an element, an <ng-template> node, or to a directive on an
    // element or template.
    if (
      (this.target instanceof TmplAstElement && !this.tcb.env.config.checkTypeOfDomReferences) ||
      !this.tcb.env.config.checkTypeOfNonDomReferences
    ) {
      // References to DOM nodes are pinned to 'any' when `checkTypeOfDomReferences` is `false`.
      // References to `TemplateRef`s and directives are pinned to 'any' when
      // `checkTypeOfNonDomReferences` is `false`.
      initializer = ts.factory.createAsExpression(
        initializer,
        ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
      );
    } else if (this.target instanceof TmplAstTemplate) {
      // Direct references to an <ng-template> node simply require a value of type
      // `TemplateRef<any>`. To get this, an expression of the form
      // `(_t1 as any as TemplateRef<any>)` is constructed.
      initializer = ts.factory.createAsExpression(
        initializer,
        ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
      );
      initializer = ts.factory.createAsExpression(
        initializer,
        this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]),
      );
      initializer = ts.factory.createParenthesizedExpression(initializer);
    }
    addParseSpanInfo(initializer, this.node.sourceSpan);
    addParseSpanInfo(id, this.node.keySpan);

    this.scope.addStatement(tsCreateVariable(id, initializer));
    return id;
  }
}

/**
 * A `TcbOp` which is used when the target of a reference is missing. This operation generates a
 * variable of type any for usages of the invalid reference to resolve to. The invalid reference
 * itself is recorded out-of-band.
 */
export class TcbInvalidReferenceOp extends TcbOp {
  constructor(
    private readonly tcb: Context,
    private readonly scope: Scope,
  ) {
    super();
  }

  // The declaration of a missing reference is only needed when the reference is resolved.
  override readonly optional = true;

  override execute(): ts.Identifier {
    const id = this.tcb.allocateId();
    this.scope.addStatement(tsCreateVariable(id, getAnyExpression()));
    return id;
  }
}
